import abc
import math
from typing import Optional

import numpy as np
import pandas as pd


class Node:

    def __init__(self, **kwargs):
        self._id = kwargs['id']
        self._name = kwargs['name']
        self._desc = kwargs.get('desc', '')
        self._weights = kwargs.get('weights', [])
        self._area = kwargs.get('area')
        self._type = kwargs['type']
        self._children = None

    def __repr__(self):
        return f'{self._id}: {self._name}'

    def __str__(self):
        return f'{self._id}: {self._name}'

    @property
    def id(self):
        return self._id

    @property
    def name(self):
        return self._name

    @name.setter
    def name(self, value):
        self._name = value

    @property
    def desc(self):
        return self._desc

    @desc.setter
    def desc(self, value):
        self._desc = value

    @property
    def weights(self):
        return self._weights

    @weights.setter
    def weights(self, value):
        if len(value) == len(self.children) \
                and (type(value) == np.ndarray or all([type(i) == int or type(i) == float for i in value])):
            self._weights = value
        else:
            raise ValueError("权重错误")

    @property
    def type(self):
        return self._type

    @property
    def area(self):
        return self._area

    @property
    def children(self):
        return [] if self._children is None else self._children

    @property
    def children_strs(self):
        if self._children:
            return [str(child) for child in self.children]
        else:
            return []

    def attr(self):
        return {
            'id': self._id,
            'name': self._name,
            'desc': self._desc,
            'weights': self._weights,
            'type': self._type,
            'children': self.children_strs
        }

    def dump_to_dict(self):
        d = self.attr()
        d['children'] = [child.dump_to_dict() for child in self.children]
        return d

    def is_valid_weight(self):
        return len(self.weights) == len(self.children) and math.isclose(sum(self.weights), 1)

    @staticmethod
    def load_from_dict(node_dict):
        # TODO 增加格式验证
        node = Node(**node_dict)
        if node._type != "leaf":
            node._children = [Node.load_from_dict(child) for child in node_dict["children"]]
        return node


class AHPModel:

    def __init__(self, model_dict):
        self._model = Node.load_from_dict(model_dict)

    def load_model_from_dict(self, model_dict):
        self._model = Node.load_from_dict(model_dict)

    @property
    def model(self):
        return self._model

    @model.setter
    def model(self, value):
        self._model = value

    def find_nodes_by_type(self, node_type="leaf", against=True):
        stack, flag = [self._model], 0
        res = []
        while flag != len(stack):
            if (stack[flag].type == node_type) ^ against:
                res.append(stack[flag])
            for child in stack[flag].children:
                stack.append(child)
            flag += 1
        return res

    @property
    def leaves(self):
        return self.find_nodes_by_type(against=False)

    @property
    def non_leaf(self):
        return self.find_nodes_by_type()

    @property
    def all_nodes(self):
        return self.find_nodes_by_type("")

    def is_valid_model(self):
        """
        验证模型有效性
        1. 直接结点是根
        2. 叶子结点没有子结点
        3. 每个非叶子节点权重都有效
        :return: 模型是否有效, 原因
        """
        if self._model is None:
            return False, "模型未加载"

        if self._model.type != "root":
            return False, f"模型的根节点类型错误：{self._model.type}"

        for node in self.all_nodes:
            if node.type != "leaf":
                if not node.is_valid_weight():
                    return False, f"结点id：{node.id}权重无效"
            else:
                if node.children:
                    return False, f"叶子结点id：{node.id}存在子结点"

        return True, "模型有效"

    def find_node_by_id(self, node_id):
        stack, flag = [self._model], 0
        while flag != len(stack):
            if stack[flag].id == node_id:
                return stack[flag].attr()
            for child in stack[flag].children:
                stack.append(child)
            flag += 1

    def set_weight_by_id(self, node_id, weight):
        stack, flag = [self._model], 0
        while flag != len(stack):
            if stack[flag].id == node_id:
                stack[flag].weights = weight
                break
            for child in stack[flag].children:
                stack.append(child)
            flag += 1


class AbstractInstance(abc.ABC):

    def __init__(self):
        self._model: Optional[AHPModel] = None
        self._alternatives = None
        self._data = None
        self._result = None
        self._calculated = False

    @property
    def calculated(self):
        return self._calculated

    @property
    def model(self):
        return self._model

    @model.setter
    def model(self, value):
        if self._data is None:
            self._model = value
        else:
            raise Exception("数据层已经设置，不可更改模型！")

    def is_valid_model(self):
        if self._model is not None:
            return self._model.is_valid_model()[0]
        else:
            raise Exception("模型尚未设置!")

    @property
    def alternatives(self):
        return self._alternatives

    @alternatives.setter
    def alternatives(self, value):
        if self._data is None:
            self._alternatives = value
        else:
            raise Exception("数据层已经设置，不可更改方案！")

    @property
    def data(self):
        return self._data

    @data.setter
    def data(self, value):
        if self._alternatives is None:
            raise Exception("方案层尚未设置，不可添加数据！")
        else:
            assert not self._calculated, "结果已计算，不可修改数据"
            self._data = value
            self._result = self._data

    @property
    def result(self):
        if self.calculated:
            return self._result
        else:
            return None

    @abc.abstractmethod
    def run(self):
        pass


class AHPInstance(AbstractInstance):

    def __init__(self):
        super(AHPInstance, self).__init__()

    def run(self):
        # DFS
        if self._model is None:
            raise Exception("没有层次分析法模型，不可运行！")
        if self._alternatives is None:
            raise Exception("没有方案，不可运行！")
        if self._data is None:
            raise Exception("没有数据，不可运行！")

        if not self.calculated:
            ## 层次分析法的可用性计算
            # 深度优先遍历，获取对应非底层准则的结点
            non_leaf_stack = self._model.non_leaf

            # 自底向上求每个结点的评分
            while non_leaf_stack:
                node = non_leaf_stack.pop()
                if node.children:
                    # 获取子结点对应准则的评分
                    criterion_data = self._result[[child.id for child in node.children]]
                    # 合并数据
                    self._result = self._result.join(criterion_data.dot(
                        # 加权求和，计算父结点的评分
                        pd.DataFrame(node.weights, index=criterion_data.columns.values, columns=[node.id]))
                    )

            self._calculated = True

        return self._result.iloc[-1]


class FAHPInstance(AbstractInstance):

    def __init__(self):
        super(FAHPInstance, self).__init__()
        self._membership = None

    @property
    def membership(self):
        return self._membership

    @membership.setter
    def membership(self, function_dict):
        if self._membership is None:
            self._membership = function_dict

    def run(self):
        # DFS
        assert self._model is not None, "没有设置层次分析法模型，不可运行！"
        assert self._alternatives is not None, "没有添加方案，不可运行！"
        assert self._data is not None, "没有导入数据，不可运行！"
        assert self._membership is not None, "没有设置隶属度函数，不可运行！"

        if not self.calculated:
            ## 模糊层次分析法的可用性计算
            # 计算底层准则对各评语的隶属度
            results = [self._result.apply(f) for f in self._membership.values()]

            # 深度优先遍历，获取对应非底层准则的结点
            non_leaf_stack = self._model.non_leaf

            # 对于每个评语，分别计算所有准则的隶属度
            for i in range(len(results)):
                non_leaf_count = len(non_leaf_stack)
                # 自底向上求每个结点的隶属度
                while non_leaf_count:
                    node = non_leaf_stack[non_leaf_count - 1]
                    if node.children:
                        # 获取子结点对应准则的隶属度
                        criterion_data = results[i][[child.id for child in node.children]]
                        # 合并数据
                        results[i] = results[i].join(criterion_data.dot(
                            # 加权求和，计算父结点的隶属度
                            pd.DataFrame(node.weights, index=criterion_data.columns.values, columns=[node.id]))
                        )
                    non_leaf_count -= 1

            # 归纳结果
            self._result = [pd.DataFrame(
                np.asarray(results).argmax(axis=0),  # 使用最大隶属度原则获得评语
                index=results[0].index,
                columns=results[0].columns
            )] + results

            self._calculated = True

        return self._result[0].iloc[-1]
